// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: linchuan

#include "coin2/exchange/korbit/order_v1/parser.h"

#include <memory>
#include <set>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include <boost/algorithm/string.hpp>
#include <boost/algorithm/string/replace.hpp>
#include <rapidjson/document.h>

#include "coin/proto/coin_query.pb.h"
#include "coin2/base/conversion.h"
#include "coin2/base/log.h"
#include "coin2/exchange/util/json_util.h"

namespace coin2::exchange::korbit::order_v1 {

namespace {

using coin::proto::AccountBalance;
using coin::proto::AccountPosition;
using coin::proto::CancelOrderResponse;
using coin::proto::FillType;
using coin::proto::ProductFillBundle;
using coin::proto::ProductFillElement;
using coin::proto::ProductOrderBundle;
using coin::proto::ProductOrderElement;
using coin::proto::SubmitOrderResponse;

using OrderContext = base::order::IOrderContext;
using symbology::KorbitCurrency;
using symbology::KorbitProduct;

using JsonValue = rapidjson::GenericValue<rapidjson::UTF8<>>;
using OrderEventType = coin::proto::OrderEvent::OrderEventType;

inline coin::proto::OrderSide GetOrderSide(const std::string_view& side) {
  if (side == "bid") {
    return coin::proto::OrderSide::BUY_ORDER;
  } else if (side == "ask") {
    return coin::proto::OrderSide::SELL_ORDER;
  } else {
    return coin::proto::OrderSide::UNKNOWN_ORDER_SIDE;
  }
}

inline bool IsKorbitErrMessage(const JsonValue& doc) {
  return doc.IsObject() && doc.HasMember("status") &&
         doc["status"].GetString() != std::string("success");
}

inline std::string GetKorbitExchangeOrderErrorMsg(const JsonValue& json) {
  return json["status"].GetString();
}

inline coin::proto::OrderErrorCode GetOrderErrorCodeFromKorbit(const JsonValue& json) {
  using coin::proto::OrderErrorCode;
  auto result = coin::proto::OrderErrorCode::ORDER_UNKNOWN_ERROR;
  std::string error_code = GetKorbitExchangeOrderErrorMsg(json);

  if (error_code == "0") {
    result = OrderErrorCode::ORDER_NO_ERROR;
  } else if (error_code.find("not_enough_") != std::string::npos) {
    result = OrderErrorCode::SUBMIT_ORDER_INSUFFICIENT_FUND;
  } else if (error_code == "already_filled") {
    result = OrderErrorCode::ORDER_NOT_FOUND;
  } else {
    LOG(ERROR) << "Unknown error code: " << error_code;
  }

  return result;
}

}  // namespace

std::string KorbitParser::ParseAuthToken(const JsonValue& doc) {
  /*
  {
    "token_type":"Bearer",
    "access_token":"1t1LgTslDrGznxPxhYz7RldsNVIbnEK",
    "expires_in":3600,
    "scope": "VIEW,TRADE",
    "refresh_token":"vn5xoOf4PzckgnqjQSL9Sb3KxWJvYtm"
  }
  */
  if (doc.IsObject() && doc.HasMember("access_token")) {
    return std::string(doc["access_token"].GetString()) + "\n" +
           std::string(doc["refresh_token"].GetString());
  }
  return "";
}

void KorbitParser::ParseRestAccountBalance() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestAccountBalance] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  auto account_balance = ParseBalance(document);
  if (!account_balance.has_exchange() || !account_balance.has_market_type()) {
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  result_.set_type(MsgType::ACCOUNT_BALANCE);
  *result_.mutable_account_balance() = std::move(account_balance);
}

// NOTE: when field exchange and market_type are set, which means parsed successfully
AccountBalance KorbitParser::ParseBalance(const JsonValue& doc) {
  /*
  {
    "aergo": {
      "available": "0",
      "trade_in_use": "0",
      "withdrawal_in_use": "0"
    },
    ...
  }
  */
  AccountBalance acc_balance;
  if (doc.IsObject() && IsKorbitErrMessage(doc)) {
    LOG(ERROR) << "[ParseBalance] " << JsonToString(doc);
    return acc_balance;
  }

  acc_balance.set_exchange("Korbit");
  acc_balance.set_market_type("Spot");
  for (const auto& info : doc.GetObject()) {
    std::string native_symbol = info.name.GetString();
    const auto& data = info.value;
    if (!symcache_->has_currency_native(native_symbol)) {
      continue;
    }
    auto* currency = symcache_->GetCurrencyByNative(native_symbol);
    auto currency_balance = acc_balance.add_each_balance();
    currency_balance->set_currency(currency->currency());
    currency_balance->set_currency_native(currency->native_currency());
    double current_free = stod(data["available"].GetString());
    double current_hold = stod(data["trade_in_use"].GetString());
    currency_balance->set_available(current_free);
    currency_balance->set_total(current_free + current_hold);
    currency_balance->set_hold(current_hold);
    DLOG(INFO) << "balance: " << currency->native_currency() << " " << currency_balance->hold()
               << "  " << current_free;
  }

  return acc_balance;
}

void KorbitParser::ParseRestOpenOrdersInfo() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestOpenOrders] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  *result_.mutable_order_list() = ParseRestOpenOrderInfoList(document);
  result_.set_type(MsgType::ORDER_STATUS);
}

void KorbitParser::ParseRestDoneOrdersInfo() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestDoneOrders] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  *result_.mutable_order_list() = ParseRestDoneOrderInfoList(document);
  result_.set_type(MsgType::ORDER_STATUS);
}

ProductOrderBundle KorbitParser::ParseRestOpenOrderInfoList(const JsonValue& data) {
  /*
  [
    {
      "timestamp": 1389173297000,
      "id": "58726",
      "type": "ask",
      "price": {
        "currency": "krw",
        "value": "800000"
      },
      "total": {
        "currency": "btc",
        "value": "1.00000000"
      },
      "open": {
        "currency": "btc",
        "value": "0.75000000"
      }
    },
    ...
  ]
  */
  ProductOrderBundle product_order_bundle;
  if (data.IsObject() && IsKorbitErrMessage(data)) {
    LOG(ERROR) << "[ParseRestOpenOrderInfoList] " << JsonToString(data);
    return product_order_bundle;
  }

  if (!data.IsArray()) {
    LOG(ERROR) << "[ParseRestOpenOrderInfoList] Unexpected order list format!"
               << JsonToString(data);
    return product_order_bundle;
  }

  for (auto& info : data.GetArray()) {
    auto order = product_order_bundle.add_each_order();
    order->set_market_type(MarketType::Spot);
    order->set_exchange_type(ExchangeType::Korbit);
    order->set_api_version("v1");
    order->set_exchange_order_id(info["id"].GetString());
    order->set_symbol(
        std::string(info["total"]["currency"].GetString()) + "_" +
        std::string(info["price"]["currency"].GetString()));
    order->set_price(stod(info["price"]["value"].GetString()));
    order->set_qty(stod(info["total"]["value"].GetString()));
    order->set_side(GetOrderSide(info["type"].GetString()));
    order->set_timestamp(msg_->timestamp);
    order->set_is_live(true);
  }
  return product_order_bundle;
}

ProductOrderBundle KorbitParser::ParseRestDoneOrderInfoList(const JsonValue& data) {
  /*
  [
    {
      "id": "89999",
      "currency_pair": "btc_krw",
      "side": "bid",
      "avg_price": "2900000",
      "price": "3000000",
      "order_amount": "0.81140000",
      "filled_amount": "0.33122200",
      "order_total": "2434200",
      "filled_total": "960543",
      "created_at": "1500033942638",
      "last_filled_at": "1500533946947",
      "status": "partially_filled",
      "fee": "0.00000500"
    },
    ...
  ]
  */
  ProductOrderBundle product_order_bundle;
  if (data.IsObject() && IsKorbitErrMessage(data)) {
    LOG(ERROR) << "[ParseRestOrderInfoList] " << JsonToString(data);
    return product_order_bundle;
  }

  if (!data.IsArray()) {
    LOG(ERROR) << "Unexpected order list format!" << JsonToString(data);
    return product_order_bundle;
  }

  for (auto& info : data.GetArray()) {
    auto order = product_order_bundle.add_each_order();
    order->set_market_type(MarketType::Spot);
    order->set_exchange_type(ExchangeType::Korbit);
    order->set_api_version("v1");
    order->set_exchange_order_id(info["id"].GetString());
    order->set_symbol(info["currency_pair"].GetString());
    order->set_price(stod(info["price"].GetString()));
    order->set_qty(stod(info["order_amount"].GetString()));
    order->set_side(GetOrderSide(info["side"].GetString()));
    order->set_timestamp(msg_->timestamp);
    std::string status = info["status"].GetString();
    if (status == "unfilled" || status == "partially_filled") {
      order->set_is_live(true);
    } else if (status == "ORDER_STATUS_REJECTED") {
      order->set_is_live(false);
    } else if (status == "canceled") {
      order->set_cancel_confirmed(true);
    } else if (status == "filled") {
      order->set_fully_filled(true);
    } else {
      LOG(ERROR) << "[ParseOrderInfo] Unknown order status! " << status;
    }
  }
  return product_order_bundle;
}

void KorbitParser::ParseRestFillInfoList() {
  /*
  [
    {
      "timestamp": 1383707746000,
      "completedAt": 1383797746000,
      "id": "599",
      "type": "sell",
      "fee":{"currency":"krw","value":"1500"},
      "fillsDetail":{
        "price": {"currency": "krw", "value": "1000000"},
        "amount": {"currency": "btc", "value": "1"},
        "native_amount": {"currency": "krw", "value": "1000000"},
        "orderId":"1002"
      }
    },
    ...
  ]
  */
  JsonDocument data_arr;
  if (!PrepareJsonDocument(&data_arr)) {
    LOG(ERROR) << "[ParseRestFillInfoList] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  if (data_arr.IsObject() && IsKorbitErrMessage(data_arr)) {
    LOG(ERROR) << "[ParseRestFillInfoList] " << JsonToString(data_arr);
    return;
  }

  if (!data_arr.IsArray()) {
    LOG(ERROR) << "[ParseRestFillInfoList] Unexpected fill list format!" << JsonToString(data_arr);
    return;
  }

  auto product_fill_bundle = result_.mutable_fill_list();
  product_fill_bundle->Clear();
  bool product_fill_bundle_found = false;
  // first fill is the newest, so parse in reverse order
  for (int i = data_arr.Size() - 1; i >= 0; i--) {
    const JsonValue& data = data_arr[i];
    auto fill = product_fill_bundle->add_each_fill();

    fill->set_symbol(
        std::string(data["fillsDetail"]["amount"]["currency"].GetString()) + "_" +
        std::string(data["fillsDetail"]["price"]["currency"].GetString()));
    fill->set_fill_timestamp(msg_->timestamp);
    fill->set_exchange_order_id(data["fillsDetail"]["orderId"].GetString());
    fill->set_price(stod(data["fillsDetail"]["price"]["value"].GetString()));
    fill->set_qty(stod(data["fillsDetail"]["amount"]["value"].GetString()));
    fill->set_fill_id(data["id"].GetString());
    if (data["fee"]["value"].GetString() == std::string("0")) {
      fill->set_fill_type(FillType::MAKER_FILL_TYPE);
    } else {
      fill->set_fill_type(FillType::TAKER_FILL_TYPE);
    }
    product_fill_bundle_found = true;
    auto fee_info = data["fee"].GetObject();
    fill->set_fee(stod(fee_info["value"].GetString()));
    fill->set_fee_currency(fee_info["currency"].GetString());
  }
  if (product_fill_bundle_found) {
    result_.set_type(MsgType::ORDER_FILL);
  }
}

void KorbitParser::ParseSubmitResponse() {
  /*
  {
    "orderId":"12513",
    "status":"success",
    "currency_pair":"btc_krw"
  }
  */
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::SUBMIT_RESPONSE);
  auto resp = result_.mutable_submit_response();
  resp->Clear();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.result_int());
  resp->set_timestamp(msg_->timestamp);

  if (http_context->status == AsioHttpContextStatus::kError) {
    LOG(ERROR) << "[KorbitSubmitOrder] AsioHttpContextStatus Error";
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    resp->set_success(false);
    return;
  }

  if (http_context->res.result_int() == 429) {
    LOG(ERROR) << "[KorbitSubmitOrder] API_LIMIT_ERROR";
    resp->set_error_code(coin::proto::OrderErrorCode::API_LIMIT_ERROR);
    resp->set_success(false);
    return;
  }

  JsonDocument json;
  if (!PrepareJsonDocument(&json)) {
    LOG(ERROR) << "[KorbitSubmitOrder] Parse Error\n" << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    resp->set_success(false);
    return;
  }
  if (IsKorbitErrMessage(json)) {
    auto error_code = GetOrderErrorCodeFromKorbit(json);
    resp->set_error_code(error_code);
    resp->set_exchange_error_msg(GetKorbitExchangeOrderErrorMsg(json));
    if (error_code != coin::proto::OrderErrorCode::ORDER_NO_ERROR) {
      LOG(ERROR) << "[KorbitSubmitOrder] Error Response" << http_context->res;
    }
    resp->set_success(false);
    return;
  }

  if (!(json.HasMember("status") && json["status"].GetString() == std::string("success"))) {
    LOG(ERROR) << "[KorbitSubmitOrder] invalid status\n" << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_UNKNOWN_ERROR);
    resp->set_success(false);
    return;
  }
  if (!json.HasMember("orderId")) {
    LOG(ERROR) << "[KorbitSubmitOrder] exchange order id not found\n" << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_UNKNOWN_ERROR);
    resp->set_success(false);
    return;
  }
  resp->set_success(true);
  resp->set_exchange_order_id(json["orderId"].GetString());
}

void KorbitParser::ParseCancelResponse() {
  /*
  [
    {"orderId":"1000","status":"success"},
    {"orderId":"1001","status":"not_found"},
    {"orderId":"1002","status":"success"}
  ]
  */
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::CANCEL_RESPONSE);
  auto resp = result_.mutable_cancel_response();
  resp->Clear();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.result_int());
  resp->set_exchange_order_id(order_context->exchange_order_id());
  resp->set_timestamp(msg_->timestamp);

  if (http_context->status == AsioHttpContextStatus::kError) {
    LOG(ERROR) << "[KorbitCancelOrder] AsioHttpContextStatus Error";
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    resp->set_success(false);
    return;
  }

  if (http_context->res.result_int() == 429) {
    LOG(ERROR) << "[KorbitCancelOrder] API_LIMIT_ERROR";
    resp->set_error_code(coin::proto::OrderErrorCode::API_LIMIT_ERROR);
    resp->set_success(false);
    return;
  }

  JsonDocument json;
  if (!PrepareJsonDocument(&json)) {
    LOG(ERROR) << "[KorbitCancelOrder] Parse Error\n" << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    resp->set_success(false);
    return;
  }

  if (IsKorbitErrMessage(json)) {
    auto error_code = GetOrderErrorCodeFromKorbit(json);
    resp->set_error_code(error_code);
    resp->set_exchange_error_msg(GetKorbitExchangeOrderErrorMsg(json));
    if (error_code != coin::proto::OrderErrorCode::ORDER_NO_ERROR) {
      LOG(ERROR) << "[KorbitCancelOrder] Error Response" << http_context->res;
    }
    resp->set_success(false);
    return;
  }

  if (!(json.IsArray() && json[0].HasMember("status") && json[0]["status"] == "success")) {
    auto error_code = GetOrderErrorCodeFromKorbit(json[0]);
    resp->set_error_code(error_code);
    resp->set_exchange_error_msg(GetKorbitExchangeOrderErrorMsg(json[0]));
    resp->set_success(false);
    return;
  }
  resp->set_success(true);
  resp->set_cancel_confirmed(true);
}

void KorbitParser::ParseWsMessage() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseWsMessage] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  DLOG(INFO) << "OG ws msg: " << JsonToString(document);
  // websocket unsupported
}

void KorbitParser::ParseAccountToken() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseAccountToken] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  auto token = ParseAuthToken(document);
  CHECK(!token.empty()) << JsonToString(document);
  mb_->PublishEvent("korbit/spot/token", &token);
  result_.set_type(MsgType::IGNORABLE);
}

auto KorbitParser::ParseRawMessage(const RawMessage& msg) -> const ParseResult& {
  result_.Clear();
  msg_ = &msg;
  sub_topic_id_ = static_cast<OrderSubTopicId>(msg.type);

  if (msg.type == OrderSubTopicId::WS_MESSAGE) {
    ParseWsMessage();
    return result_;
  }

  switch (msg.http_context->query_type) {
    case OrderSubTopicId::LISTEN_KEY:
      ParseAccountToken();
      break;
    case OrderSubTopicId::SUBMIT_RESPONSE:
      ParseSubmitResponse();
      break;
    case OrderSubTopicId::CANCEL_RESPONSE:
      ParseCancelResponse();
      break;
    case OrderSubTopicId::MULTI_CANCEL_RESPONSE:
      // pass
      break;
    case OrderSubTopicId::REST_OPEN_ORDERS:
      ParseRestOpenOrdersInfo();
      break;
    case OrderSubTopicId::REST_DONE_ORDERS:
      ParseRestDoneOrdersInfo();
      break;
    case OrderSubTopicId::REST_FILL_ORDERS:
      ParseRestFillInfoList();
      break;
    case OrderSubTopicId::REST_ACCOUNT_BALANCE:
      ParseRestAccountBalance();
      break;
    default: {
      LOG(ERROR) << "Parsing Error. " << msg_->DebugString();
      result_.set_error_code(OrderErrorCode::ORDER_UNKNOWN_ERROR);
      break;
    }
  }

  return result_;
}

}  // namespace coin2::exchange::korbit::order_v1
